// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.missile;

import bats.SpatialProperties;
import bats.control.physics.BatsBubbleControl;
import bats.scene.SceneNode;
import bats.unit.UnitClass;
import com.jme3.audio.AudioNode;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.PhysicsTickListener;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.PhysicsControl;
import com.jme3.bullet.objects.PhysicsGhostObject;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.controls.AudioControl;
import jme3utilities.physics.OverlapListener;

/**
 * A physics control for a shaft which has encountered an obstruction.
 * <p>
 * The shaft is minimally effected by the obstruction until it makes contact
 * with the obstruction's geometry. On first contact, the shaft can either
 * penetrate the obstruction or bounce back, depending on its orientation and
 * velocity and the obstruction's hardness.
 * <p>
 * Upon penetrating an obstruction, the shaft meets resistance. It rapidly loses
 * momentum. It may become stuck in the obstruction, or it may pass through.
 * <p>
 * Penetration is determined by ray casting in geometry space. This prevents
 * shafts from getting stuck outside the obstruction's mesh, even when the
 * obstacle's collision shape is larger than its geometry.
 * <p>
 * Note that while this is a physics control, it is neither a GhostControl nor a
 * RigidBodyControl. It uses a ghost physics object to steer its spatial, making
 * it a peculiar hybrid of the two.
 * <p>
 * Each instance is disabled at creation.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class PenetrateControl
        extends PhysicsGhostObject
        implements OverlapListener, PhysicsControl, PhysicsTickListener {
    // *************************************************************************
    // constants

    /**
     * elasticity of rebounds: 0=inelastic 1=perfectly elastic
     */
    final private static float reboundElasticity = 0.2f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(PenetrateControl.class.getName());
    // *************************************************************************
    // fields
    /**
     * true if the shaft's collision shape is so far clear of overlapping
     * objects, false if an overlapping object has been found
     */
    private boolean clearSoFar = false;
    /**
     * true if the control is enabled, false if it is disabled
     */
    private boolean enabled = false;
    /**
     * true if there's been a physics tick since the last update
     */
    private boolean hasTicked = false;
    /**
     * true of the shaft's geometry is penetrating the obstruction's geometry,
     * false if it isn't
     */
    private boolean penetration = false;
    /**
     * true if the shaft follows the obstruction, false if it moves by its own
     * momentum
     */
    private boolean relativeMode = false;
    /**
     * seconds remaining before resistance overcomes the shaft's momentum
     * (except in relative mode)
     */
    private float penetrateSeconds = 0f;
    /**
     * which physics space contains this control
     */
    private PhysicsSpace physicsSpace = null;
    /**
     * the shaft's orientation relative to the obstruction (relative mode only)
     */
    private Quaternion relativeOrientation = null;
    /**
     * which scene contains the shaft, used for ray casting and gravity: set by
     * constructor
     */
    final private SceneNode scene;
    /**
     * the spatial being penetrated, or null if there's none
     */
    private Spatial obstruction = null;
    /**
     * the shaft's spatial
     */
    private Spatial spatial = null;
    /**
     * in relative mode, the shaft's location relative to the obstruction (in
     * world units)
     */
    private Vector3f relativeLocation = null;
    /**
     * the shaft's velocity, except in relative mode (world coordinates)
     */
    private Vector3f velocity = null;
    // *************************************************************************
    // constructors

    /**
     * Instantiate a disabled control with the specified collision shape.
     *
     * @param shape the shaft's collision shape (not null)
     * @param scene which scene the shaft is in (not null)
     */
    public PenetrateControl(CollisionShape shape, SceneNode scene) {
        super(shape);
        assert shape != null;

        assert scene != null;
        this.scene = scene;

        assert !isEnabled();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Initialize and enable this control.
     *
     * @param newVelocity (in world units/second, not null, unaffected)
     * @param newObstruction (not null)
     */
    public void enable(Vector3f newVelocity, Spatial newObstruction) {
        assert !enabled;
        assert newObstruction != null;
        assert newVelocity != null;

        obstruction = newObstruction;
        velocity = newVelocity.clone();

        setEnabled(true);
    }

    /**
     * Test whether the control is enabled.
     *
     * @return true if the control is enabled, false otherwise
     */
    @Override
    final public boolean isEnabled() {
        return enabled;
    }
    // *************************************************************************
    // Control methods

    /**
     * Clone this control for a different spatial.
     *
     * @param spatial which spatial to clone for (not null)
     * @return a new control
     */
    @Override
    public Control cloneForSpatial(Spatial spatial) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    /**
     * Callback to perform any rendering this control requires.
     *
     * @param renderManager
     * @param viewPort
     */
    @Override
    public void render(RenderManager renderManager, ViewPort viewPort) {
        /* no rendering required */
    }

    /**
     * Add the control to a spatial and initialize it, or remove the control
     * from its spatial.
     *
     * Before adding this control, its physics space must be set.
     *
     * @param spatial if null, the control is removed
     */
    @Override
    public void setSpatial(Spatial spatial) {
        assert physicsSpace != null;

        this.spatial = spatial;
        setUserObject(spatial);
        if (spatial == null) {
            // TODO remove tick listener?
            return;
        }
        /*
         * Copy the spatial's location and orientation to the physical object.
         */
        Vector3f location = spatial.getWorldTranslation().clone();
        setPhysicsLocation(location);
        Quaternion orientation = spatial.getWorldRotation().clone();
        setPhysicsRotation(orientation);

        physicsSpace.addTickListener(this);
    }

    /**
     * Update this control. (Invoked once per frame.)
     *
     * @param unused simulation seconds since the previous update (&gt;0)
     */
    @Override
    public void update(float unused) {
        assert spatial != null;
        if (!enabled || !scene.isEnabled()) {
            return;
        }
        /*
         * The first update after each physics tick is the only
         * update which matters.
         */
        if (!hasTicked) {
            return;
        }
        hasTicked = false;
        /*
         * If there is no overlapping physical object and no
         * geometric penetration, then this control is done.
         */
        if (clearSoFar && !penetration) {
            done();
            return;
        }
        clearSoFar = true;
        if (relativeMode) {
            logger.log(Level.FINE, "rel loc = {0}", relativeLocation);
            spatial.setLocalTranslation(relativeLocation);
            spatial.setLocalRotation(relativeOrientation);

        } else {
            /*
             * The physical object drives the spatial.
             *
             * Note that this is the reverse of GhostControl,
             * where the spatial drives the physical object.
             */
            Vector3f location = getPhysicsLocation().clone();
            MySpatial.setWorldLocation(spatial, location);
            Quaternion orientation = getPhysicsRotation().clone();
            MySpatial.setWorldOrientation(spatial, orientation);
        }
        /*
         * If the shaft penetrates the obstruction's geometry,
         * re-parent the shaft's spatial to the obstruction.
         */
        if (!penetration) {
            return;
        }
        Node newParent;
        if (obstruction instanceof Node) {
            newParent = (Node) obstruction;
        } else {
            newParent = obstruction.getParent();
        }
        assert newParent != null;
        MySpatial.adopt(newParent, spatial);
        // TODO maintain location and orientation also?
        /*
         * If the obstruction is a unit, impair the unit.
         */
        UnitClass unitClass = scene.getWorld().getUnitClass(newParent);
        if (unitClass != null) {
            unitClass.impair(newParent);
        }
        if (velocity != null) {
            assert velocity.lengthSquared() > 0f;
            return;
        }
        logger.log(Level.FINE, "{0} is stuck in {1}", new Object[]{
            spatial.getName(), obstruction.getName()
        });
        /*
         * If the obstruction is a unit, switch this control to relative mode.
         */
        if (SpatialProperties.isUnit(obstruction)) {
            goRelativeMode();
            return;
        }
        /*
         * Since the obstruction is not a unit, switch to bubble control.
         */
        logger.log(Level.INFO, "enable bubble for {0}", spatial.getName());
        BatsBubbleControl bubbleControl = spatial.getControl(BatsBubbleControl.class);
        bubbleControl.setEnabled(true);
        setEnabled(false);
    }
    // *************************************************************************
    // OverlapListener methods

    /**
     * React to an overlapping rigid body.
     *
     * @param overlappingBody the overlapping rigid body (not null)
     * @param overlappingSpatial the spatial of the overlapping rigid body (not
     * null)
     * @param localPoint the location of the overlap (rotated and translated to
     * this control's object, but at world scale)
     */
    @Override
    public void onOverlap(PhysicsRigidBody overlappingBody,
            Spatial overlappingSpatial, Vector3f localPoint) {
        assert overlappingBody != null;
        assert overlappingSpatial != null;
        assert localPoint != null;

        if (!isEnabled()) {
            return;
        }
        logger.log(Level.FINE, "not clear {0}", overlappingSpatial.getName());
        clearSoFar = false;
    }
    // *************************************************************************
    // PhysicsControl methods

    /**
     * Get the physics space of this control.
     *
     * @return the pre-existing instance or null
     */
    @Override
    public PhysicsSpace getPhysicsSpace() {
        return physicsSpace;
    }

    /**
     * Enable or disable this control.
     *
     * The physics space and spatial must both be set. Disabling the control
     * removes it from physics space.
     *
     * @param newState if true, enable the control; if false, disable it
     */
    @Override
    public void setEnabled(boolean newState) {
        assert physicsSpace != null;
        assert spatial != null;

        if (newState && !enabled) {
            clearSoFar = false;
            relativeLocation = null;
            relativeMode = false;
            relativeOrientation = null;
            /*
             * Copy the spatial's location and rotation to this
             * control's physics state.
             */
            Vector3f location = spatial.getWorldTranslation().clone();
            setPhysicsLocation(location);
            Quaternion orientation = spatial.getWorldRotation().clone();
            setPhysicsRotation(orientation);
            /*
             * Add this control to physics space.
             */
            physicsSpace.addCollisionObject(this);

        } else if (!newState && enabled) {
            /*
             * Remove this control from physics space.
             */
            physicsSpace.removeCollisionObject(this);
            obstruction = null;
            velocity = null;
        }
        enabled = newState;
    }

    /**
     * Change which physics space contains this control.
     *
     * @param newSpace
     */
    @Override
    public void setPhysicsSpace(PhysicsSpace newSpace) {
        if (newSpace == physicsSpace) {
            return;
        }
        if (enabled) {
            physicsSpace.removeCollisionObject(this);
            if (newSpace != null) {
                newSpace.addCollisionObject(this);
            }
        }
        physicsSpace = newSpace;
    }
    // *************************************************************************
    // PhysicsGhostObject methods

    /**
     * De-serialize this control when loading from a J3O file.
     *
     * @param importer (not null)
     * @throws IOException
     */
    @Override
    public void read(JmeImporter importer)
            throws IOException {
        super.read(importer);
        InputCapsule capsule = importer.getCapsule(this);
        penetrateSeconds = capsule.readFloat("resistSeconds", 0f);
        velocity = (Vector3f) capsule.readSavable("velocity", null);
        enabled = capsule.readBoolean("isEnabled", false);
        spatial = (Spatial) capsule.readSavable("spatial", null);
        throw new UnsupportedOperationException("Not supported yet.");
    }

    /**
     * Serialize this control when saving to a J3O file.
     *
     * @param exporter (not null)
     * @throws IOException
     */
    @Override
    public void write(JmeExporter exporter)
            throws IOException {
        super.write(exporter);
        OutputCapsule capsule = exporter.getCapsule(this);
        capsule.write(penetrateSeconds, "resistSeconds", 0f);
        capsule.write(velocity, "velocity", null);
        capsule.write(enabled, "isEnabled", false);
        capsule.write(spatial, "spatial", null);
        throw new UnsupportedOperationException("Not supported yet.");
    }
    // *************************************************************************
    // PhysicsTickListener methods

    /**
     * Method invoked after each physics tick.
     *
     * @param unused
     * @param unused2
     */
    @Override
    public void physicsTick(PhysicsSpace unused, float unused2) {
        if (spatial == null || !isEnabled() || !scene.isEnabled()) {
            return;
        }

        hasTicked = true;
    }

    /**
     * Method invoked before each physics tick to test for penetration and
     * update the shaft's velocity and location.
     *
     * @param unused
     * @param simInterval seconds since previous invocation
     */
    @Override
    public void prePhysicsTick(PhysicsSpace unused, float simInterval) {
        if (spatial == null || !enabled || !scene.isEnabled()) {
            return;
        }
        if (relativeMode) {
            assert penetration;
            /*
             * The obstruction's motion drives the physical object.
             */
            Vector3f location = spatial.getWorldTranslation().clone();
            setPhysicsLocation(location);
            Quaternion orientation = spatial.getWorldRotation().clone();
            setPhysicsRotation(orientation);
        }
        checkForPenetration();
        if (relativeMode) {
            assert penetration;
            return;
        }
        if (penetration) {
            /*
             * Shaft loses some momentum to the obstruction's resistance.
             */
            resistance(simInterval);
            if (relativeMode) {
                return;
            }
        }
        // TODO gravity
        /*
         * Shaft moves due to its residual momentum.
         */
        assert velocity.lengthSquared() > 0f;
        Vector3f deltaX = velocity.mult(simInterval);
        Vector3f location = getPhysicsLocation().clone();
        location.addLocal(deltaX);
        setPhysicsLocation(location);
    }
    // *************************************************************************
    // private methods

    /**
     * Check whether the shaft is penetrating the obstruction's geometry.
     * (Invoked before the physics tick.)
     */
    private void checkForPenetration() {
        //logger.log(Level.INFO, "");
        /*
         * Create a geometric ray in the point direction with its
         * vertex at the trailing end of the shaft.
         */
        Vector3f pointDirection = getPointDirection();
        Shafts shafts = scene.getWorld().getShafts();
        float shaftLength = shafts.getLength(spatial); // meters
        float halfLength = shaftLength / 2f; // meters
        float scaleFactor = scene.getScaleFactor();
        Vector3f tipOffset = pointDirection.mult(halfLength / scaleFactor);
        Vector3f location = getPhysicsLocation().clone();
        Vector3f tail = location.subtract(tipOffset);
        Ray ray = new Ray(tail, pointDirection);
        /*
         * Search for penetrated geometries by casting the ray down the
         * length of the shaft.
         */
        boolean isPenetrating = false;
        CollisionResults results = scene.getWorld().collideWith(ray);
        int count = results.size();
        for (int collisionIndex = 0; collisionIndex < count; collisionIndex++) {
            CollisionResult contact = results.getCollision(collisionIndex);
            /*
             * Ignore any ray collision past the tip of the shaft.
             */
            float distance = contact.getDistance() * scaleFactor; // meters
            if (distance > shaftLength) {
                continue;
            }
            /*
             * Ignore any self-collision.
             */
            Geometry geometry = contact.getGeometry();
            assert geometry != null;
            Spatial collidingSpatial = SpatialProperties.findResistor(geometry);
            assert collidingSpatial != null;
            if (collidingSpatial == spatial) {
                continue;
            }
            logger.log(Level.INFO, "geometry={0} {1}", new Object[]{
                geometry.getName(), penetration
            });
            if (collidingSpatial == obstruction) {
                if (penetration) {
                    isPenetrating = true;
                } else {
                    isPenetrating = newContact(contact);
                }
            } else if (penetration) {
                /*
                 * The ray collided with a third-party spatial:  switch
                 * this control to relative mode.
                 */
                goRelativeMode();
                /*
                 * Slide the shaft forward or backward to attempt to
                 * eliminate the collision.
                 */
                float advance;
                if (distance > halfLength) {
                    /*
                     * Slide the shaft backward to eliminate the tip overlap.
                     */
                    advance = distance - shaftLength; // meters
                } else {
                    /*
                     * Slide the shaft forward to eliminate the tail overlap.
                     */
                    advance = distance; // meters
                }
                Vector3f direction = relativeOrientation.mult(Vector3f.UNIT_Z);
                // TODO limit rate of slide?
                Vector3f offset = direction.mult(advance / scaleFactor);
                relativeLocation.addLocal(offset);
            }
        }
        penetration = isPenetrating;
        if (relativeMode) {
            penetration = true; // The geometry must've changed.
        }
    }

    /**
     * Transfer control back to ShaftControl.
     */
    private void done() {
        logger.log(Level.INFO, "");
        /*
         * There are no longer any overlapping objects in physics space.
         * Perhaps the shaft missed the mesh, rebounded off it, or passed
         * all the way through it.
         *
         * Enable shaft control (for gravity and collisions) and
         * position it to match this control.
         */
        Vector3f location = getPhysicsLocation().clone();
        Quaternion orientation = getPhysicsRotation().clone();
        Vector3f shaftVelocity = Vector3f.ZERO;
        if (velocity != null) {
            shaftVelocity = velocity;
        }
        ShaftControl shaftControl = spatial.getControl(ShaftControl.class);
        shaftControl.enable(location, orientation, shaftVelocity);
        /*
         * Disable this control.
         */
        setEnabled(false);
    }

    /**
     * Get the shaft's +Z direction in world coordinates.
     *
     * @return a new unit vector
     */
    private Vector3f getPointDirection() {
        Quaternion orientation = getPhysicsRotation();
        Vector3f direction = orientation.mult(Vector3f.UNIT_Z);
        return direction;
    }

    /**
     * Switch this control to relative mode.
     */
    private void goRelativeMode() {
        assert enabled;
        assert penetration;
        if (relativeMode) {
            /*
             * If already in relative mode, this method has no effect.
             */
            return;
        }
        logger.log(Level.INFO, "for {0}", spatial.getName());
        assert velocity != null;
        assert relativeLocation == null : relativeLocation;
        assert relativeOrientation == null : relativeOrientation;
        velocity = null;
        relativeMode = true;
        penetrateSeconds = 0f;
        /*
         * Save the position and orientation of the shaft
         * relative to the obstruction.
         */
        Vector3f location = getPhysicsLocation();
        Node parent = spatial.getParent(); // TODO: obstruction?
        relativeLocation = parent.worldToLocal(location, null);
        Quaternion orientation = getPhysicsRotation();
        Quaternion inverse = MySpatial.inverseOrientation(parent);
        relativeOrientation = inverse.mult(orientation);
    }

    /**
     * Simulate a new contact with the obstruction to decide whether the shaft
     * will penetrate or not. (Invoked before the physics tick.)
     *
     * @param collisionResult from the geometric collision test (not null)
     * @return true if the shaft penetrates the obstruction, false if it
     * rebounds
     */
    private boolean newContact(CollisionResult collisionResult) {
        assert collisionResult != null;
        assert !relativeMode;
        Geometry geometry = collisionResult.getGeometry();
        assert geometry != null;
        logger.log(Level.INFO, "{0} geometry={1}", new Object[]{
            spatial.getName(), geometry.getName()
        });
        /*
         * If the shaft is flying backward, it does not penetrate.
         */
        Vector3f pointDirection = getPointDirection();
        assert pointDirection.isUnitVector();
        if (velocity.dot(pointDirection) < 0f) {
            logger.log(Level.INFO, "{0} is flying backward",
                    spatial.getName());
            return false;
        }
        /*
         * If the magnitude of the normal component of the shaft's velocity
         * exceeds the obstruction's hardness, then the shaft begins
         * penetrating the obstruction.
         */
        Vector3f normalDirection = collisionResult.getContactNormal();
        assert normalDirection.isUnitVector();
        float normalWuPerSecond = velocity.dot(normalDirection);
        logger.log(Level.INFO, "velocity={0}, normalDir={1}, normalComp={2}",
                new Object[]{
            velocity, normalDirection, normalWuPerSecond
        });
        float hardness = SpatialProperties.getHardness(geometry);
        logger.log(Level.INFO, "hardness={0} m/s", hardness);
        float scaleFactor = scene.getScaleFactor();
        float normalV = FastMath.abs(normalWuPerSecond * scaleFactor);
        logger.log(Level.INFO, "normal={0} m/s", normalV);
        if (normalV > hardness) {
            penetrateSeconds = 0.05f; // TODO
            logger.log(Level.INFO, "{0} penetrates {1}", new Object[]{
                spatial.getName(),
                obstruction.getName()
            });
            return true;
        }
        if (normalWuPerSecond > 0f) {
            logger.log(Level.INFO, "{0} already hit {1} ?", new Object[]{
                spatial.getName(),
                obstruction.getName()
            });
            return false;
        }
        /*
         * The shaft rebounds off the obstruction.
         */
        float reboundGain = -(1f + reboundElasticity);
        Vector3f deltaWuPerSecond =
                normalDirection.mult(reboundGain * normalWuPerSecond);
        velocity.addLocal(deltaWuPerSecond);
        logger.log(Level.INFO, "{0} rebounds off {1}", new Object[]{
            spatial.getName(),
            obstruction.getName()
        });
        scene.getWorld().getShafts().rebound(spatial);
        /*
         * Play the rebound sound effect.
         */
        Node node = (Node) spatial;
        AudioNode tinkNode = (AudioNode) node.getChild(Shafts.tinkPath);
        tinkNode.setPitch(0.5f);
        tinkNode.setVelocity(velocity);
        float mass = MySpatial.getMass(spatial);
        float soundVolume = normalV * mass / 30f;
        logger.log(Level.INFO, "volume={0}", soundVolume);
        tinkNode.setVolume(soundVolume);
        AudioControl audioControl = tinkNode.getControl(AudioControl.class);
        audioControl.playInstance();

        return false;
        // TODO transfer the change in momentum to the obstruction
    }

    /**
     * Simulate the obstruction resisting the shaft. (Invoked before the physics
     * tick.)
     *
     * @param simInterval seconds since previous tick (&ge;0)
     */
    private void resistance(float simInterval) {
        assert penetration;
        assert !relativeMode;
        assert simInterval >= 0f : simInterval;
        assert penetrateSeconds > 0f : penetrateSeconds;
        /*
         * The shaft decelerates at a constant rate for
         * the remainder of its resistance interval.
         */
        float oldSeconds = penetrateSeconds;
        penetrateSeconds -= simInterval;
        if (penetrateSeconds > 0f) {
            velocity.multLocal(penetrateSeconds / oldSeconds);
        } else {
            goRelativeMode();
        }
        // TODO transfer lost momentum to the obstruction
    }
}